﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;

namespace NaturalNote.Core.Mapper
{
    /// <summary>
    /// object mapper
    /// </summary>
    public static class SMapper
    {
        // -- begin --
        public static void Map<S, T>(S s, T t)
        {
            Mapper<S, T>.Instance.Map(s, t);
        }
        public static T Map<S, T>(S s)
        {
            return Mapper<S, T>.Instance.Map(s);
        }
        // -- end --

        internal static Func<S, T> GetMapper<S, T>()
        {
            return Mapper<S, T>.Instance.Map;
        }
        internal static Action<S, T> GetAcMapper<S, T>()
        {
            return Mapper<S, T>.Instance.Map;
        }
        internal static void _Map<S,T>(S s, T t)
        {
            if (s == null)
            {
                return;
            }
            Mapper<S, T>.Instance.Map(s, t);
        }  
 
        internal static bool IsCollection(Type type, out Type itemType)
        {
            itemType = null;
            if (!type.IsGenericType)
            {
                return false;
            }

            if (!typeof(ICollection<>).MakeGenericType(type.GenericTypeArguments).IsAssignableFrom(type))
            {
                return false;
            }
            itemType = type.GenericTypeArguments.FirstOrDefault();
            return true;
        }
     
        internal static bool IsDictionary(Type type, out Type k, out Type v)
        {
            k = null;
            v = null;
            if (type.GenericTypeArguments.Length != 2)
            {
                return false;
            }
            if (!typeof(IDictionary<,>).MakeGenericType(type.GenericTypeArguments).IsAssignableFrom(type))
            {
                return false;
            }
            k = type.GenericTypeArguments[0];
            v = type.GenericTypeArguments[1];
            return true;
        }
        internal static T MapValue<T>(T obj)
        {
            return obj;
        }
        internal static void MapValueList<T>(IEnumerable<T> source, ICollection<T> target)
        {
            if (source == null)
            {
                return;
            }
            foreach (var item in source)
            {
                target.Add(item);
            }
        }
        internal static void MapList<S, T>(IEnumerable<S> source, ICollection<T> target, Action<S, T> ac)
        {
            if (source == null)
            {
                return;
            }
            foreach (var item in source)
            {
                var t = Activator.CreateInstance<T>();
                ac(item, t);
                target.Add(t);
            }
        }
        internal static T[] MapArray<S, T>(S[] source, Func<S, T> func)
        {
            if (source == null || source.Length == 0)
            {
                return new T[0];
            }
            var rs = new T[source.Length];
            for (int i = 0; i < source.Length; i++)
            {
                rs[i] = func(source[i]);
            }

            return rs;
        }
        internal static void MapDic<K, S, T>(IDictionary<K, S> source, IDictionary<K, T> target, Func<S, T> mapper)
        {
            if (source == null)
            {
                return;
            }
            foreach (var item in source)
            {
                target[item.Key] = mapper(item.Value);
            }
        }
        internal static MethodInfo GetMapperMethod(Type s, Type t)
        {
            var methodInfo = typeof(SMapper).GetMethod(nameof(_Map), BindingFlags.Static | BindingFlags.NonPublic)
               .MakeGenericMethod(s, t);
            return methodInfo;
        }
        internal static MethodInfo GetMapMethod(Type s, Type t)
        {
            var methodInfo = typeof(SMapper).GetMethod(nameof(GetMapper), BindingFlags.Static | BindingFlags.NonPublic)
                .MakeGenericMethod(s, t);
            return methodInfo;
        }
        internal static MethodInfo GetAcMapMethod(Type s, Type t)
        {
            var methodInfo = typeof(SMapper).GetMethod(nameof(GetAcMapper), BindingFlags.Static | BindingFlags.NonPublic)
                .MakeGenericMethod(s, t);
            return methodInfo;
        }
        internal static MethodInfo GetMapValueMethod(Type t)
        {
            var methodInfo = typeof(SMapper).GetMethod(nameof(MapValue), BindingFlags.Static | BindingFlags.NonPublic)
                .MakeGenericMethod(t);
            return methodInfo;
        }
        internal static MethodInfo GetMapValueListMethod(Type t)
        {
            var methodInfo = typeof(SMapper).GetMethod(nameof(MapValueList), BindingFlags.Static | BindingFlags.NonPublic)
                .MakeGenericMethod(t);
            return methodInfo;
        }
        internal static MethodInfo GetMapListMethod(Type s, Type t)
        {
            var methodInfo = typeof(SMapper).GetMethod(nameof(MapList), BindingFlags.Static | BindingFlags.NonPublic)
                .MakeGenericMethod(s, t);
            return methodInfo;
        }
        internal static MethodInfo GetMapArrayMethod(Type s, Type t)
        {
            var methodInfo = typeof(SMapper).GetMethod(nameof(MapArray), BindingFlags.Static | BindingFlags.NonPublic)
                .MakeGenericMethod(s, t);
            return methodInfo;
        }
        internal static MethodInfo GetMapDicMethod(Type k, Type s, Type t)
        {
            var methodInfo = typeof(SMapper).GetMethod(nameof(MapDic), BindingFlags.Static | BindingFlags.NonPublic)
                .MakeGenericMethod(k, s, t);
            return methodInfo;
        } 
        internal static void GenMapDicMethodILCode(ILGenerator il, Type k, Type s, Type t, PropertyInfo source, PropertyInfo target)
        {
            // Func<sv,tv> func = null;
            //1. func = SMapper.MapValue<sv>;
            //2. func = SMapper.GetMapper<sv,tv>();
            // SMapper.MapDic<k,s,t>(source,target, func);
            var mapmethod = GetMapDicMethod(k, s, t);

            var mapperFuncType = typeof(Func<,>).MakeGenericType(s, t);
            var dicvar = il.DeclareLocal(target.PropertyType);
            var mapperFuncLoc = il.DeclareLocal(mapperFuncType);
            il.Emit(OpCodes.Newobj, target.PropertyType.GetConstructor(Type.EmptyTypes));
            il.Emit(OpCodes.Stloc_S, dicvar);

            if (TypeHelper.IsValueType(s) && s == t)
            {
                var mapper = GetMapValueMethod(s);
                il.Emit(OpCodes.Ldnull);
                il.Emit(OpCodes.Ldftn, mapper);
                il.Emit(OpCodes.Newobj, mapperFuncType.GetConstructor(new Type[] { typeof(object), typeof(IntPtr) }));
                il.Emit(OpCodes.Stloc_S, mapperFuncLoc);
            }
            else
            {
                var getMapper = GetMapMethod(s, t);
                il.Emit(OpCodes.Call, getMapper);
                il.Emit(OpCodes.Stloc_S, mapperFuncLoc);
            }
            il.Emit(OpCodes.Ldarg_0); //source
            il.Emit(OpCodes.Callvirt, source.GetMethod);
            il.Emit(OpCodes.Ldloc_S, dicvar);
            il.Emit(OpCodes.Ldloc_S, mapperFuncLoc);
            il.Emit(OpCodes.Call, mapmethod);

            il.Emit(OpCodes.Ldarg_1); // target
            il.Emit(OpCodes.Ldloc_S, dicvar);
            il.Emit(OpCodes.Callvirt, target.SetMethod);
        }
        internal static void GenMapListMethodILCode(ILGenerator il,
            PropertyInfo source, PropertyInfo target,
            Type sourceArgType, Type targetArgType)
        {

            var isnulllab = il.DefineLabel();
            var targetListLocal = il.DeclareLocal(target.PropertyType);
            // target.xx = null;
            il.Emit(OpCodes.Ldarg_1); // target
            il.Emit(OpCodes.Ldnull);
            il.Emit(OpCodes.Callvirt, target.SetMethod);
            // if(source.xx == null) 跳过拷贝值
            il.Emit(OpCodes.Ldarg_0); //source
            il.Emit(OpCodes.Callvirt, source.GetMethod);
            il.Emit(OpCodes.Ldnull);
            il.Emit(OpCodes.Ceq);
            il.Emit(OpCodes.Brtrue_S, isnulllab);

            // targetListLocal = new List<T>();

            il.Emit(OpCodes.Newobj, target.PropertyType.GetConstructor(Type.EmptyTypes));
            il.Emit(OpCodes.Stloc_S, targetListLocal);

            //MapValueList(source.list, targetlistlocal)
            //target.list = targetlistlocal;
            il.Emit(OpCodes.Ldarg_0); //source
            il.Emit(OpCodes.Callvirt, source.GetMethod);
            il.Emit(OpCodes.Ldloc_S, targetListLocal);
            if (TypeHelper.IsValueType(sourceArgType) && sourceArgType == targetArgType)
            {
                var mapvalueListFunc = GetMapValueListMethod(sourceArgType);
                il.Emit(OpCodes.Call, mapvalueListFunc);
            }
            else
            {
                //var actype = typeof(Action<,>).MakeGenericType(sourceArgType, targetArgType);
                //var acLoc = il.DeclareLocal(actype);
                var getMapper = GetAcMapMethod(sourceArgType, targetArgType);
                il.Emit(OpCodes.Call, getMapper);
                //il.Emit(OpCodes.Stloc_S, acLoc);
                var mapList = GetMapListMethod(sourceArgType, targetArgType);
                //il.Emit(OpCodes.Ldloc_S, acLoc);
                il.Emit(OpCodes.Call, mapList);
            }

            il.Emit(OpCodes.Ldarg_1); // target
            il.Emit(OpCodes.Ldloc_S, targetListLocal);
            il.Emit(OpCodes.Callvirt, target.SetMethod);

            il.MarkLabel(isnulllab);
        }
        internal static void GenMapArrayMethodILCode(ILGenerator il,
            PropertyInfo source, PropertyInfo target,
            Type sourceArgType, Type targetArgType)
        {
            var mapperFuncType = typeof(Func<,>).MakeGenericType(sourceArgType, targetArgType);
            var mapperFuncLoc = il.DeclareLocal(mapperFuncType);

            if (TypeHelper.IsValueType(targetArgType) && targetArgType == sourceArgType)
            {
                var mapper = GetMapValueMethod(targetArgType);
                il.Emit(OpCodes.Ldnull);
                il.Emit(OpCodes.Ldftn, mapper);
                il.Emit(OpCodes.Newobj, mapperFuncType.GetConstructor(new Type[] { typeof(object), typeof(IntPtr) }));
                il.Emit(OpCodes.Stloc_S, mapperFuncLoc);
            }
            else
            {
                var getMapper = GetMapMethod(sourceArgType, targetArgType);
                il.Emit(OpCodes.Call, getMapper);
                il.Emit(OpCodes.Stloc_S, mapperFuncLoc);
            }
            //*
            var maplistmethod = GetMapArrayMethod(sourceArgType, targetArgType);
            il.Emit(OpCodes.Ldarg_1); // target
            il.Emit(OpCodes.Ldarg_0); //source
            il.Emit(OpCodes.Callvirt, source.GetMethod);
            il.Emit(OpCodes.Ldloc_S, mapperFuncLoc);
            il.Emit(OpCodes.Call, maplistmethod);
            il.Emit(OpCodes.Callvirt, target.SetMethod);
        }
        internal static void GenMapMethodILCode(ILGenerator il,
            PropertyInfo source, PropertyInfo target)
        {
            /*
             * 
                static void TestGen2(t1 a, t2 b)
                {
                    b.a = new t7();
                    SMapper._Map(a.a, b.a);
                }
             * */
            var methodInfo = GetMapperMethod(source.PropertyType, target.PropertyType);

            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Newobj, target.PropertyType.GetConstructor(Type.EmptyTypes));
            il.Emit(OpCodes.Callvirt, target.SetMethod);
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Callvirt, source.GetMethod);
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Callvirt, source.GetMethod);
            il.Emit(OpCodes.Call, methodInfo);
                        
        }

    }
    internal class Mapper<S, T>
    {
        public void Map(S s, T t)
        {
            if (s == null)
            {
                return;
            }
            dynamicMethod.DynamicInvoke(s, t);
        }

        public T Map(S s)
        {
            var t = Activator.CreateInstance<T>();
            this.Map(s, t);
            return t;
        }

        private Delegate dynamicMethod;

        private void buildMethod()
        {
            var sourceType = typeof(S);
            var targetType = typeof(T);
            var method = new DynamicMethod("_map", null, new Type[] { sourceType, targetType}, typeof(Mapper<S, T>));


            var dicSourceProp = new Dictionary<string, PropertyInfo>();

            foreach (var propertyInfo in sourceType.GetProperties())
            {
                dicSourceProp[propertyInfo.Name] = propertyInfo;
            }

            var il = method.GetILGenerator();

            foreach (var targetItem in targetType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
            {
                if (!dicSourceProp.ContainsKey(targetItem.Name))
                {
                    continue;
                }

                var attr = targetItem.GetCustomAttributes(typeof(SkipMapAttribute), true);
                if (attr != null && attr.Any())
                {
                    continue;
                }

                var sourceItem = dicSourceProp[targetItem.Name];

                if (!(sourceItem.CanRead && targetItem.CanWrite))
                {
                    continue;
                }

                if (TypeHelper.IsValueType(targetItem.PropertyType) && sourceItem.PropertyType == targetItem.PropertyType)
                {
                    // target.item = source.item;
                    il.Emit(OpCodes.Ldarg_1); // target
                    il.Emit(OpCodes.Ldarg_0); //source
                    il.Emit(OpCodes.Callvirt, sourceItem.GetMethod);
                    il.Emit(OpCodes.Callvirt, targetItem.SetMethod);
                    continue;
                }

                Type sk, tk, sv, tv;

                if (SMapper.IsDictionary(sourceItem.PropertyType, out sk, out sv)
                    && SMapper.IsDictionary(targetItem.PropertyType, out tk, out tv) && sk == tk)
                {
                    if (sk != tk)
                    {
                        continue;
                    }
                    SMapper.GenMapDicMethodILCode(il, sk, sv, tv, sourceItem, targetItem);
                    continue;
                }
                Type source_list_item_type;
                Type target_list_item_type;
                if (TypeHelper.IsEnumerable(sourceItem.PropertyType, out source_list_item_type)
                    && SMapper.IsCollection(targetItem.PropertyType, out target_list_item_type))
                {
                    SMapper.GenMapListMethodILCode(il, sourceItem, targetItem, source_list_item_type, target_list_item_type);
                    continue;
                }
                Type source_array_type;
                Type target_array_type;
                if (TypeHelper.IsArray(sourceItem.PropertyType, out source_array_type)
                    && TypeHelper.IsArray(targetItem.PropertyType, out target_array_type))
                {
                    SMapper.GenMapArrayMethodILCode(il, sourceItem, targetItem, source_array_type, target_array_type);
                    continue;
                }
                if (sourceItem.PropertyType.IsClass && targetItem.PropertyType.IsClass)
                {
                    SMapper.GenMapMethodILCode(il, sourceItem, targetItem);
                }

            }

            il.Emit(OpCodes.Ret);


            dynamicMethod = method.CreateDelegate(typeof(Action<,>).MakeGenericType(sourceType, targetType));
        }

        private Mapper() { buildMethod(); }

        class Nested
        {
            static Nested() { }
            public static readonly Mapper<S, T> Instance = new Mapper<S, T>();
        }

        public static Mapper<S, T> Instance { get { return Nested.Instance; } }

    }

}
